/*
    SDL - Simple DirectMedia Layer
    Partial implementation of SDL library (originally written by
    Sam Lantinga <slouken@libsdl.org>) for the particular purpose to support
    Prequengine (http://code.google.com/p/prequengine/) on BlackBerry(tm)
    tablets and smartphones.

    Copyright (C) 2013  xlamsp

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License along
    with this program; if not, write to the Free Software Foundation, Inc.,
    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

    xlamsp@gmail.com
*/

#include "SDL_mixer.h"
#include "SDL_timer.h"
#include "timidity.h"

int volatile music_active = 1;
static int volatile music_stopped = 0;
static int music_loops = 0;
static Mix_Music * volatile music_playing = NULL;
static int music_volume = SDL_MIX_MAX_VOLUME;

struct _Mix_Music
{
    Mix_MusicType type;
    union {
        MidiSong *midi;
    } data;
    Mix_Fading fading;
    int fade_step;
    int fade_steps;
    int error;
};

static int timidity_ok;
static int samplesize;

/* Used to calculate fading steps */
static int ms_per_step;


/* Halt playing of music */
static void music_internal_halt(void)
{
    SDL_Log(DEBUG1, "[MUS] entered");

    switch (music_playing->type) {
        case MUS_MID:
        if(timidity_ok) {
            Timidity_Stop();
        }
        break;

        default:
        /* Unknown music type?? */
        return;
    }

    music_playing->fading = MIX_NO_FADING;
    music_playing = NULL;
}

/* Set the music volume */
static void music_internal_volume(int volume)
{
    SDL_Log(DEBUG1, "[MUS] entered, volume=%d", volume);

    switch (music_playing->type) {
        case MUS_MID:
        if(timidity_ok) {
            Timidity_SetVolume(volume);
        }
        break;

        default:
        /* Unknown music type?? */
        break;
    }
}

/* Set the music's initial volume */
static void music_internal_initialize_volume(void)
{
    SDL_Log(DEBUG1, "[MUS] entered");

    if(music_playing->fading == MIX_FADING_IN) {
        music_internal_volume(0);
    } else {
        music_internal_volume(music_volume);
    }
}

/* Play a music chunk.  Returns 0, or -1 if there was an error.
 */
static int music_internal_play(Mix_Music *music)
{
    int retval = 0;

    SDL_Log(DEBUG1, "[MUS] entered");

    /* Note the music we're playing */
    if(music_playing) {
        music_internal_halt();
    }
    music_playing = music;

    /* Set the initial volume */
    if(music->type != MUS_MOD) {
        music_internal_initialize_volume();
    }

    /* Set up for playback */
    switch (music->type) {
        case MUS_MID:
        if(timidity_ok) {
            Timidity_Start(music->data.midi);
        }
        break;

        default:
        SDL_Log(ERROR, "Can't play unknown music type");
        retval = -1;
        break;
    }

    /* If the setup failed, we're not playing any music anymore */
    if(retval < 0) {
        music_playing = NULL;
    }

    return retval;
}

/* Check the status of the music */
static int music_internal_playing()
{
    int playing = 1;

    SDL_Log(DEBUG1, "[MUS] entered");

    switch (music_playing->type) {
        case MUS_MID:
        if(timidity_ok) {
            if(!Timidity_Active()) {
                playing = 0;
            }
        }
        break;

        default:
        playing = 0;
        break;
    }

    return playing;
}

/* If music isn't playing, halt it if no looping is required, restart it */
/* otherwhise. NOP if the music is playing */
static int music_halt_or_loop (void)
{
    SDL_Log(DEBUG1, "[MUS] entered");

    /* Restart music if it has to loop */
    if(!music_internal_playing())
    {
        /* Restart music if it has to loop at a high level */
        if (music_loops && --music_loops) {
            Mix_Fading current_fade = music_playing->fading;
            music_internal_play(music_playing);
            music_playing->fading = current_fade;
        } else {
            music_internal_halt();
            return 0;
        }
    }

    return 1;
}

/* Mixing function */
void music_mixer(void *udata, Uint8 *stream, int len)
{
    int left = 0;

    if(music_playing && music_active) {
        /* Handle fading */
        if(music_playing->fading != MIX_NO_FADING) {
            if(music_playing->fade_step++ < music_playing->fade_steps) {
                int volume;
                int fade_step = music_playing->fade_step;
                int fade_steps = music_playing->fade_steps;

                if(music_playing->fading == MIX_FADING_OUT) {
                    volume = (music_volume * (fade_steps-fade_step)) / fade_steps;
                } else { /* Fading in */
                    volume = (music_volume * fade_step) / fade_steps;
                }
                music_internal_volume(volume);
            } else {
                if(music_playing->fading == MIX_FADING_OUT) {
                    music_internal_halt();
                    return;
                }
                music_playing->fading = MIX_NO_FADING;
            }
        }

        if(music_halt_or_loop() == 0) {
            return;
        }

        switch (music_playing->type) {
            case MUS_MID:
                if(timidity_ok) {
                    int samples = len / samplesize;
                    Timidity_PlaySome(stream, samples);
                }
                break;

            default:
                /* Unknown music type?? */
                break;
        }
    }

    /* Handle seamless music looping */
    if(left > 0 && left < len && music_halt_or_loop()) {
        music_mixer(udata, stream + (len - left), left);
    }
}

/* Initialize the music players with a certain desired audio format */
int open_music(SDL_AudioSpec *spec)
{
    SDL_Log(DEBUG1, "[MUS] entered, freq=%d format=0x%x channels=%u samples=%u size=%u",
        spec->freq, (Uint32)spec->format, (Uint32)spec->channels, (Uint32)spec->samples, spec->size);

    samplesize = spec->size / spec->samples;

    if(Timidity_Init(spec->freq, spec->format, spec->channels, spec->samples) == 0) {
        timidity_ok = 1;
    } else {
        timidity_ok = 0;
    }

    music_playing = NULL;
    music_stopped = 0;
    Mix_VolumeMusic(SDL_MIX_MAX_VOLUME);

    /* Calculate the number of ms for each callback */
    ms_per_step = ((int)spec->samples * 1000) / spec->freq;

    return 0;
}

/* Uninitialize the music players */
void close_music(void)
{
    SDL_Log(DEBUG1, "[MUS] entered");

    Mix_HaltMusic();
    Timidity_Close();
}


Mix_Music *Mix_LoadMUS(const char *file)
{
    #warning: stub function, not implemented yet!
    SDL_Log(DEBUG1, "[MUS] file=%s", file);
    return NULL;
}

void Mix_FreeMusic(Mix_Music *music)
{
    SDL_Log(DEBUG1, "[MUS] music=0x%p", music);

    if(music) {
        /* Stop the music if it's currently playing */
        SDL_LockAudio();
        if(music == music_playing) {
            /* Wait for any fade out to finish */
            while(music->fading == MIX_FADING_OUT) {
                SDL_UnlockAudio();
                SDL_Delay(100);
                SDL_LockAudio();
            }
            if(music == music_playing) {
                music_internal_halt();
            }
        }
        SDL_UnlockAudio();

        switch (music->type) {
            case MUS_MID:
                if(timidity_ok) {
                    Timidity_FreeSong(music->data.midi);
                }
                break;

            default:
                /* Unknown music type?? */
                break;
        }
        SDL_free(music);
    }
}

int Mix_FadeInMusic(Mix_Music *music, int loops, int ms)
{
    int retval;

    SDL_Log(DEBUG1, "[MUS] music=0x%p loops=%d ms=%d", music, loops, ms);

    /* Don't play null pointers :-) */
    if(music == NULL) {
        SDL_Log(ERROR, "music parameter was NULL");
        return -1;
    }

    /* Setup the data */
    if(ms) {
        music->fading = MIX_FADING_IN;
    } else {
        music->fading = MIX_NO_FADING;
    }
    music->fade_step = 0;
    music->fade_steps = ms / ms_per_step;

    /* Play the puppy */
    SDL_LockAudio();
    /* If the current music is fading out, wait for the fade to complete */
    while(music_playing && (music_playing->fading == MIX_FADING_OUT)) {
        SDL_UnlockAudio();
        SDL_Delay(100);
        SDL_LockAudio();
    }
    music_active = 1;
    music_loops = loops;
    retval = music_internal_play(music);
    SDL_UnlockAudio();

    return retval;
}

int Mix_PlayMusic(Mix_Music *music, int loops)
{
    SDL_Log(DEBUG1, "[MUS] music=0x%p loops=%d", music, loops);
    return Mix_FadeInMusic(music, loops, 0);
}

int Mix_VolumeMusic(int volume)
{
    int prev_volume;

    SDL_Log(DEBUG1, "[MUS] volume=%d", volume);

    prev_volume = music_volume;
    if(volume < 0) {
        return prev_volume;
    }

    if(volume > SDL_MIX_MAX_VOLUME) {
        volume = SDL_MIX_MAX_VOLUME;
    }

    music_volume = volume;
    SDL_LockAudio();
    if(music_playing) {
        music_internal_volume(music_volume);
    }
    SDL_UnlockAudio();

    return prev_volume;
}

int Mix_HaltMusic(void)
{
    SDL_Log(DEBUG1, "[MUS] entered");

    SDL_LockAudio();
    if(music_playing) {
        music_internal_halt();
    }
    SDL_UnlockAudio();

    return 0;
}

int Mix_FadeOutMusic(int ms)
{
    int retval = 0;

    SDL_Log(DEBUG1, "[MUS] ms=%d", ms);

    if(ms <= 0) {  /* just halt immediately. */
        Mix_HaltMusic();
        return 1;
    }

    SDL_LockAudio();
    if(music_playing) {
        int fade_steps = (ms + ms_per_step - 1) / ms_per_step;
        if(music_playing->fading == MIX_NO_FADING) {
            music_playing->fade_step = 0;
        } else {
            int step;
            int old_fade_steps = music_playing->fade_steps;
            if(music_playing->fading == MIX_FADING_OUT) {
                step = music_playing->fade_step;
            } else {
                step = old_fade_steps - music_playing->fade_step + 1;
            }
            music_playing->fade_step = (step * fade_steps) / old_fade_steps;
        }
        music_playing->fading = MIX_FADING_OUT;
        music_playing->fade_steps = fade_steps;
        retval = 1;
    }
    SDL_UnlockAudio();

    return retval;
}

void Mix_RewindMusic(void)
{
    #warning: stub function, not implemented yet!
    SDL_Log(DEBUG1, "[MUS]");
    return;
}

int Mix_PlayingMusic(void)
{
    int playing = 0;

    SDL_Log(DEBUG1, "[MUS] entered");

    SDL_LockAudio();
    if(music_playing) {
        playing = music_internal_playing();
    }
    SDL_UnlockAudio();

    return playing;
}

Mix_Music *Mix_LoadMUS_RW(SDL_RWops *rw)
{
    Uint8 magic[5];
    Mix_Music *music;
    int start;

    SDL_Log(DEBUG1, "[MUS] rw=0x%p", rw);

    if(!rw) {
        SDL_Log(ERROR, "RWops pointer is NULL");
        return NULL;
    }

    /* Figure out what kind of file this is */
    start = SDL_RWtell(rw);
    if(SDL_RWread(rw, magic, 1, 4) != 4) {
        SDL_Log(ERROR, "Couldn't read from RWops");
        return NULL;
    }
    SDL_RWseek(rw, start, RW_SEEK_SET);
    magic[4] = '\0';

    /* Allocate memory for the music structure */
    music = (Mix_Music *)SDL_malloc(sizeof(Mix_Music));
    if(music == NULL ) {
        SDL_Log(ERROR, "Out of memory");
        return(NULL);
    }
    music->error = 0;

    /* MIDI files have the magic four bytes "MThd" */
    if(strcmp((char *)magic, "MThd") == 0) {
        music->type = MUS_MID;
        if(timidity_ok) {
            music->data.midi = Timidity_LoadSong_RW(rw, 0);
            if(music->data.midi == NULL) {
                SDL_Log(ERROR, "%s", Timidity_Error());
                music->error = 1;
            }
        } else {
            SDL_Log(ERROR, "%s", Timidity_Error());
            music->error = 1;
        }
    } else {
        SDL_Log(ERROR, "Unrecognized music format");
        music->error=1;
    }

    if(music->error) {
        SDL_free(music);
        music = NULL;
    }

    return(music);
}

